//
//  PSScorer.m
//  PrefCore
//
//  Created by Sergey on 2/28/14.
//  Copyright (c) 2014 Play-on-smart. All rights reserved.
//

#import "PSScorer.h"
#import "PSCalculator.h"

@interface PSPassingScorer ()

/// Find minimum number of tricks in the last round of the game.
+(PSTrickCount)minTricksInGame:(PSGame*)game;

@end

@interface ScorerInstances : NSObject

@property (nonatomic, readonly) PSMisereScorer *misereScorer;

@property (nonatomic, readonly) PSPassingScorer *passingScorer;

@property (nonatomic, readonly) PSPassingRostovScorer *passingRostovScorer;

@property (nonatomic, readonly) PSTrickingScorer *trickingScorer;

@property (nonatomic, readonly) PSTrickingTenScorer *trickingTenScorer;

@property (nonatomic, readonly) PSTrickingHalfWhistScorer *trickingHalfWhistScorer;

@property (nonatomic, readonly) PSTrickingTwoPassesScorer *trickingTwoPassesScorer;

@property (nonatomic, readonly) PSTrickingWithoutThreeScorer *trickingWithoutThreeScorer;

@end

@implementation ScorerInstances

-(instancetype)init {
    if (self = [super init]) {
        _misereScorer = [[PSMisereScorer alloc] init];
        _passingScorer = [[PSPassingScorer alloc] init];
        _passingRostovScorer = [[PSPassingRostovScorer alloc] init];
        _trickingScorer = [[PSTrickingScorer alloc] init];
        _trickingTenScorer = [[PSTrickingTenScorer alloc] init];
        _trickingHalfWhistScorer = [[PSTrickingHalfWhistScorer alloc] init];
        _trickingTwoPassesScorer = [[PSTrickingTwoPassesScorer alloc] init];
        _trickingWithoutThreeScorer = [[PSTrickingWithoutThreeScorer alloc] init];
    }
    return self;
}

@end

@implementation PSScorer

+(void)balanceGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    // obtain mountain delta
    uint8_t nf = calc.normalizationFactor;
    
    for (PSPlayer *p in game.players) {
        PSScore *score = [game scoreForPlayerId:p.identifier];
        score.mountain.delta = nf * (settings.poolSize - score.pool.current);
    }
    // find minimal mountain
    int64_t minMountain = INT64_MAX;
    for (PSPlayer *p in game.players) {
        minMountain = MIN(minMountain, [game scoreForPlayerId:p.identifier].mountain.eventual);
    }
    // amnesty + write final whists
    for (PSPlayer *p in game.players) {
        PSScore *score = [game scoreForPlayerId:p.identifier];
        score.mountain.delta -= minMountain;
        if (score.mountain.eventual > 0) {
            int64_t whists = 10 * score.mountain.eventual / PS_GAME_PLAYERS;
            for (PSPlayer *other = [game nextPlayer:p] ; other != p ; other = [game nextPlayer:other]) {
                PSScore *otherScore = [game scoreForPlayerId:other.identifier];
                otherScore.whists[p.identifier].delta = whists;
            }
        } else {
            // zero the eventual mountain value
            score.mountain.delta = -score.mountain.current;
            // zero whist delta on player who has 0 eventual mountain.
            // (otherwise some player might hold delta value from previous set's calculation)
            for (PSPlayer *other = [game nextPlayer:p] ; other != p ; other = [game nextPlayer:other]) {
                PSScore *otherScore = [game scoreForPlayerId:other.identifier];
                otherScore.whists[p.identifier].delta = 0;
            }
        }
    }
    // calculate balance for each player
    for (PSPlayer *p in game.players) {
        PSScore *score = [game scoreForPlayerId:p.identifier];
        uint64_t old = score.balance.current;
        score.balance.current = 0;
        for (PSPlayer *other = [game nextPlayer:p] ; other != p ; other = [game nextPlayer:other]) {
            PSScore *otherScore = [game scoreForPlayerId:other.identifier];
            score.balance.current += score.whists[other.identifier].eventual
                                   - otherScore.whists[p.identifier].eventual;
        }
        // update balance delta
        score.balance.delta = score.balance.current - old;
        // update statistics
        game.statistics.score[p.identifier] = score.balance.current;
    }
}

-(void)scoreGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    for (PSPlayer *p in game.players) {
        [[game scoreForPlayerId:p.identifier] settle];
    }
    [self updateGame:game withSettings:settings andCalculator:calc];
    [PSScorer balanceGame:game withSettings:settings andCalculator:calc];
    [self updateStatisticsOfGame:game];
}

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    NSAssert(NO, @"Call of an abstract method.");
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    NSAssert(NO, @"Call of an abstract method.");
}

+(ScorerInstances*)instances {
    static dispatch_once_t onceToken;
    static ScorerInstances *instanceHolder;
    dispatch_once(&onceToken, ^{
        instanceHolder = [[ScorerInstances alloc] init];
    });
    return instanceHolder;
}

+(PSMisereScorer*)misereScorer {
    return [self instances].misereScorer;
}

+(PSPassingScorer*)passingScorer {
    return [self instances].passingScorer;
}

+(PSPassingRostovScorer*)passingRostovScorer {
    return [self instances].passingRostovScorer;
}

+(PSTrickingScorer*)trickingScorer {
    return [self instances].trickingScorer;
}

+(PSTrickingTenScorer*)trickingTenScorer {
    return [self instances].trickingTenScorer;
}

+(PSTrickingHalfWhistScorer*)trickingHalfWhistScorer {
    return [self instances].trickingHalfWhistScorer;
}

+(PSTrickingTwoPassesScorer*)trickingTwoPassesScorer {
    return [self instances].trickingTwoPassesScorer;
}

+(PSTrickingWithoutThreeScorer*)trickingWithoutThreeScorer {
    return [self instances].trickingWithoutThreeScorer;
}

@end

@implementation PSMisereScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    if (game.declarerTricks == 0) {
        // in case the declarer took no tricks write into his pool
        [calc writeToPoolOfPlayer:game.declarer
                             game:game
                         settings:settings
                            value:[calc playCostSuccessWithTricks:10]];
    } else {
        // otherwise punish for every trick he took
        [calc writeToMountainOfPlayer:game.declarer
                                 game:game
                                value:game.declarerTricks * [calc playCostFailureWithTricks:10]];
    }
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    PSPlayerId decl = game.declarer.identifier;
    PSStatistics *stats = game.statistics;
    stats.misere[decl].sets++;
    if (game.declarerTricks == 0) {
        stats.misere[decl].success++;
    } else {
        stats.misere[decl].tricks += game.declarerTricks;
    }
}

@end

@implementation PSPassingScorer

+(PSScoreValue)trickCostsForGame:(PSGame*)game withSettings:(PSGameSettings*)settings {
    PSScoreValue trickCost = settings.passingTrickCost + 1;
    PSScoreValue cps = game.consecutivePassingSets;
    switch (settings.passingProgression) {
      case kPassingProgression_1111:   return trickCost;
      case kPassingProgression_123123: return trickCost * (cps % 3 + 1);
      case kPassingProgression_1233:   return trickCost * (cps < 2 ? cps + 1 : 3);
      case kPassingProgression_1244:   return trickCost * (cps < 2 ? cps + 1 : 4);
      case kPassingProgression_1234:   return trickCost * (cps + 1);
      case kPassingProgression_1248:   return trickCost * (1 << (cps > 8 ? 8 : cps));
    }
    NSAssert(NO, @"Invalid passing progression detected.");
}

+(PSTrickCount)minTricksInGame:(PSGame*)game {
    PSTrickCount res = PS_BID_MAX;
    for (PSPlayer *p in game.players) {
        res = MIN(res, [game tricksOfPlayer:p.identifier]);
    }
    return res;
}

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    PSTrickCount minTricks = [PSPassingScorer minTricksInGame:game];
    PSScoreValue cost = [PSPassingScorer trickCostsForGame:game withSettings:settings];
    for (PSPlayer *p in game.players) {
        PSTrickCount tricks = [game tricksOfPlayer:p.identifier];
        if (!tricks) {
            // in case player took no tricks write into his pool
            [calc writeToPoolOfPlayer:p game:game settings:settings value:cost];
        } else {
            // otherwise punish for every trick he took (perform amnesty by reducing minTricks)
            [calc writeToMountainOfPlayer:p game:game value:cost * (tricks - minTricks)];
        }
    }
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    game.statistics.sets.passing++;
    for (PSPlayer *p in game.players) {
        game.statistics.passingTricks[p.identifier] += [game tricksOfPlayer:p.identifier];
    }
}

@end

@implementation PSPassingRostovScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    PSTrickCount minTricks = [PSPassingScorer minTricksInGame:game];
    NSMutableArray *min = [NSMutableArray array];
    NSMutableArray *other = [NSMutableArray array];
    for (PSPlayer *p in game.players) {
        PSTrickCount tricks = [game tricksOfPlayer:p.identifier];
        // in case player took no tricks write cost of 6 tricking game into his pool
        if (!tricks) {
            PSTrickCount costOfSix = [calc playCostSuccessWithTricks:6];
            [calc writeToPoolOfPlayer:p game:game settings:settings value:costOfSix];
        }
        // add the player to one of the two arrays
        [((tricks == minTricks) ? min : other) addObject:p];
    }
    // write punishment whists
    for (PSPlayer *from in min) {
        for (PSPlayer *to in other) {
            int32_t cost = PS_ROSTOV_PASSING_TRICK_COST * [game tricksOfPlayer:to.identifier] / min.count;
            [calc writeWhistsFromPlayer:from toPlayer:to game:game value:cost];
        }
    }
}

@end

/// Called when both defenders play whists - the "whists" parameter is a number of whists of the player
static int GetMissedWhists(PSTrickCount esw, PSTrickCount whists, PSTrickCount partnerWhists) {
    if (whists >= esw) {
        return 0;
    }
    return partnerWhists <= esw ? esw - whists : esw - whists - (partnerWhists - esw);
}

/// Called when only one player said whist (no matter if playing open or standing game)
static void DistributeWhists(
    PSGame *game
,   PSGameSettings *settings
,   PSCalculator *calc
,   PSPlayer *whister
,   PSPlayer *passer
,   int contract
,   PSTrickCount missed
) {
    PSTrickCount wpTicks = [game tricksOfPlayer:whister.identifier] + [game tricksOfPlayer:passer.identifier];
    PSScoreValue costSuccess = [calc whistCostSuccessWithTricks:contract];
    PSScoreValue costMissed = [calc missedTrickCostWithTricks:contract];
    switch (settings.whistType) {
        case kWhistGreedy:
            [calc writeWhistsFromPlayer:whister
                               toPlayer:game.declarer
                                   game:game
                                  value:costSuccess * wpTicks + costMissed * missed];
            [calc writeWhistsFromPlayer:passer toPlayer:game.declarer game:game value:costMissed * missed];
            break;
            
        case kWhistGentleman: {
            PSScoreValue whists = costSuccess * wpTicks + 2 * costMissed * missed;
            [calc writeWhistsFromPlayer:whister toPlayer:game.declarer game:game value:whists / 2];
            [calc writeWhistsFromPlayer:passer toPlayer:game.declarer game:game value:whists / 2];
            break;
        }
    }
}

@implementation PSTrickingScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    PSPlayer *p2 = [game nextPlayer:game.declarer];
    PSPlayer *p3 = [game nextPlayer:p2];
    PSTrickCount contract = game.contract;
    PSTrickCount tricks2 = [game tricksOfPlayer:p2.identifier];
    PSTrickCount tricks3 = [game tricksOfPlayer:p3.identifier];
    PSTrickCount whists = tricks2 + tricks3;
    PSScoreValue whistCostSuccess = [calc whistCostSuccessWithTricks:contract];
    PSScoreValue whistCostFailure = [calc whistCostFailureWithTricks:contract];
    if (game.declarerTricks >= contract) {
        // declarer managed to take tricks by  contract (at least)
        PSTrickCount eow = [PSCalculator  expectedOpenWhistsForContract:contract];
        // write score to declarer's pool
        PSScoreValue poolPoints = [calc playCostSuccessWithTricks:contract];
        [calc writeToPoolOfPlayer:game.declarer game:game settings:settings value:poolPoints];
        // both whisters are managed to take a minimum required whists
        if (whists >= eow) {
            if (game.whisters == 2) {
                [calc writeWhistsFromPlayer:p2
                                   toPlayer:game.declarer
                                       game:game
                                      value:tricks2*whistCostSuccess];
                [calc writeWhistsFromPlayer:p3
                                   toPlayer:game.declarer
                                       game:game
                                      value:tricks3*whistCostSuccess];
            } else {
                [calc writeWhistsFromPlayer:game.opponent
                                   toPlayer:game.declarer
                                       game:game
                                      value:whists*whistCostSuccess];
            }
        } else if (game.whisters == 2) {
            // one (or both whisters) failed to take all whists
            PSTrickCount esw = [PSCalculator expectedStandingWhistsForContract:contract];
            int missedWhists2 = GetMissedWhists(esw, tricks2, tricks3);
            int missedWhists3 = GetMissedWhists(esw, tricks3, tricks2);
            [calc writeWhistsFromPlayer:p2
                               toPlayer:game.declarer
                                   game:game
                                  value:tricks2*whistCostSuccess];
            [calc writeWhistsFromPlayer:p3
                               toPlayer:game.declarer
                                   game:game
                                  value:tricks3*whistCostSuccess];
            // write to mountain of the first whister only if contact is less than 8,
            // otherwise only second whister has to be punished
            if (missedWhists2 > 0 && contract < 8) {
                [calc writeToMountainOfPlayer:p2 game:game value:whistCostFailure * missedWhists2];
            }
            if (missedWhists3 > 0) {
                [calc writeToMountainOfPlayer:p3 game:game value:whistCostFailure * missedWhists3];
            }
        } else {
            [calc writeWhistsFromPlayer:game.opponent
                               toPlayer:game.declarer
                                   game:game
                                  value:whistCostSuccess * whists];
            [calc writeToMountainOfPlayer:game.opponent
                                     game:game
                                    value:whistCostFailure * (eow - whists)];
        }
    } else {
        // declarer failed to take tricks by contract
        PSTrickCount missed = contract - game.declarerTricks;
        PSScoreValue costMissedGame = [calc playCostFailureWithTricks:contract];
        [calc writeToMountainOfPlayer:game.declarer game:game value:costMissedGame * missed];
        // both said whist
        if (game.whisters == 2) {
            PSScoreValue costMissedTrick = [calc missedTrickCostWithTricks:contract];
            [calc writeWhistsFromPlayer:p2
                               toPlayer:game.declarer
                                   game:game
                                  value:whistCostSuccess * tricks2 + costMissedTrick * missed];
            [calc writeWhistsFromPlayer:p3
                               toPlayer:game.declarer
                                   game:game
                                  value:whistCostSuccess * tricks3 + costMissedTrick * missed];
        } else {
            DistributeWhists(game, settings, calc, game.opponent, game.passer, contract, missed);
        }
    }
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    PSPlayer *p2 = [game nextPlayer:game.declarer];
    PSPlayer *p3 = [game nextPlayer:p2];
    PSTrickCount contract = game.contract;
    [game.statistics  updateTrickingForPlayerId:game.declarer.identifier
                                       contract:contract
                                         tricks:game.declarerTricks];

    if ([game bidForPlayerId:p2.identifier].current.type == kBidWhist) {
        game.statistics.whists[p2.identifier].sets++;
    } else {
        game.statistics.passes[p2.identifier]++;
    }
    if ([game bidForPlayerId:p3.identifier].current.type == kBidWhist) {
        game.statistics.whists[p3.identifier].sets++;
    } else {
        game.statistics.passes[p3.identifier]++;
    }

    // declarer managed to take all tricks (at least)
    if (game.declarerTricks >= contract) {
        int tricks2 = [game tricksOfPlayer:p2.identifier];
        int tricks3 = [game tricksOfPlayer:p3.identifier];
        int eow = [PSCalculator expectedOpenWhistsForContract:contract];
        // both whisters are managed to take a minimum required whists
        if (tricks2 + tricks3 >= eow) {
            if (game.whisters == 2) {
                game.statistics.whists[p2.identifier].success++;
                game.statistics.whists[p3.identifier].success++;
            } else {
                game.statistics.whists[game.opponent.identifier].success++;
            }
        } else {
            // one (or both whisters) failed to take all whists
            if (game.whisters == 2) {
                int esw = [PSCalculator expectedStandingWhistsForContract:contract];
                int missedWhists2 = GetMissedWhists(esw, tricks2, tricks3);
                int missedWhists3 = GetMissedWhists(esw, tricks3, tricks2);
                if (missedWhists2 == 0 || contract >= 8) {
                    game.statistics.whists[p2.identifier].success++;
                }
                if (missedWhists3 == 0) {
                    game.statistics.whists[p3.identifier].success++;
                }
            }
        }
    } else {
        if (game.whisters == 2) {
            game.statistics.whists[p2.identifier].success++;
            game.statistics.whists[p3.identifier].success++;
        } else {
            game.statistics.whists[game.opponent.identifier].success++;
        }
    }
}

@end

@implementation PSTrickingTenScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    if (game.declarerTricks == 10) {
        PSScoreValue cost = [calc playCostSuccessWithTricks:game.contract];
        [calc writeToPoolOfPlayer:game.declarer game:game settings:settings value:cost];
    } else {
        PSScoreValue cost = [calc playCostFailureWithTricks:game.contract];
        PSScoreValue missedTricks = 10 - game.declarerTricks;
        PSPlayer *declarer = game.declarer;
        PSPlayer *p1 = [game previousPlayer:declarer];
        PSPlayer *p2 = [game nextPlayer:declarer];
        [calc writeToMountainOfPlayer:declarer game:game value:cost * missedTricks];
        [calc writeWhistsFromPlayer:p1 toPlayer:declarer game:game value:cost * missedTricks];
        [calc writeWhistsFromPlayer:p2 toPlayer:declarer game:game value:cost * missedTricks];
    }
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    [game.statistics updateTrickingForPlayerId:game.declarer.identifier
                                      contract:game.contract
                                        tricks:game.declarerTricks];
}

@end

@implementation PSTrickingHalfWhistScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    PSTrickCount contract = game.contract;
    // write to declarer pool
    [calc writeToPoolOfPlayer:game.declarer
                         game:game
                     settings:settings
                        value:[calc playCostSuccessWithTricks:contract]];
    // write half whist to half whister
    PSScoreValue whists = [calc whistCostSuccessWithTricks:contract] *
                          [PSCalculator expectedStandingWhistsForContract:contract];
    [calc writeWhistsFromPlayer:game.half toPlayer:game.declarer game:game value:whists];
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    for (PSPlayer *p in game.players) {
        if (p == game.declarer) {
            [game.statistics updateTrickingForPlayerId:p.identifier
                                              contract:game.contract
                                                tricks:game.contract];
        } else {
            game.statistics.passes[p.identifier]++;
        }
    }
}

@end

@implementation PSTrickingTwoPassesScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    [calc writeToPoolOfPlayer:game.declarer
                         game:game
                     settings:settings
                        value:[calc playCostSuccessWithTricks:game.contract]];
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    PSTrickCount contract = game.contract;
    for (PSPlayer *p in game.players) {
        if (p == game.declarer) {
            // add contract tricks to aggregation and not a declarer tricks (as it equal to 10)
            [game.statistics updateTrickingForPlayerId:p.identifier contract:contract tricks:contract];
        } else {
            game.statistics.passes[p.identifier]++;
        }
    }
}

@end

@implementation PSTrickingWithoutThreeScorer

-(void)updateGame:(PSGame*)game withSettings:(PSGameSettings*)settings andCalculator:(PSCalculator*)calc {
    // write to mountain weight of 3 tricks
    [calc writeToMountainOfPlayer:game.declarer
                             game:game
                            value:3 * [calc playCostFailureWithTricks:game.contract]];
}

-(void)updateStatisticsOfGame:(PSGame*)game {
    game.statistics.withoutThree[game.player.declarer]++;
    [game.statistics updateTrickingForPlayerId:game.player.declarer
                                      contract:game.contract
                                        tricks:game.contract - 3];
}

@end
